4장. 결합과 모듈성

출처: 『소프트웨어 설계의 결합 균형』(블라드 코노노프 지음, 장연호 옮김, 제이펍 2026) | 공식: https://www.jpub.kr/

"95%의 단어가 모듈성의 이점을 극찬하는 데 쓰이고, 모듈성을 달성하는 방법에 대해서는 거의 언급되지 않는다." — Myers(1979). 40년이 지난 지금도 유효한 말이다. 이 장은 모듈이 무엇인지, 어떻게 잘 만드는지, 그리고 결합이 모듈성에서 어떤 역할을 하는지 정밀하게 다룬다.


학습 목표

이 장을 끝내면 다음을 할 수 있다.

  • 모듈성(modularity)을 정의하고, 구성요소(component)와 모듈(module)의 차이를 설명한다.
  • 모듈의 세 가지 속성(기능·로직·맥락)을 실제 코드에서 구분한다.
  • 깊은 모듈과 얕은 모듈을 인터페이스 설계로 판별하고 개선 방향을 제시한다.
  • 정보 은닉(information hiding) 원칙을 모듈 설계에 적용하고 파나스의 개념을 인용한다.
  • 누출 추상화(leaking abstraction)를 실제 예시에서 식별하고 수정한다.
  • 결합이 모듈성에서 맡는 역할을 분석하고 응집력과의 관계를 설명한다.

전체 흐름도

[ 모듈성이란? — 시스템이 미래 변화에 적응할 수 있도록 하는 설계 ]
        │
        ▼
[ 모듈 = 잘 정의된 기능을 캡슐화하는 경계 ]
   레고 블록 / 교환식 카메라 렌즈 / USB 포트 / 전기 콘센트 비유
        │
        ▼
[ 모듈의 세 가지 속성 ]
   기능(function)  ── 공개 인터페이스, 외부에 드러남
   로직(logic)     ── 구현 세부사항, 외부에 숨겨짐
   맥락(context)   ── 환경 가정, 암묵적으로 존재
        │
        ▼
[ 효과적인 모듈 설계 원칙 — 파나스의 정보 은닉 ]
   깊은 모듈 (good)  vs  얕은 모듈 (bad)
   추상화 = 공유하는 세부사항에만 초점, 불필요한 것은 숨김
        │
        ▼
[ 모듈성 vs 복잡성의 대립 ]
   큰 진흙덩이 안티패턴 / 과도한 유연성의 비용 / 누출 추상화
        │
        ▼
[ 결합이 모듈성에서 하는 역할 ]
   결합 = 구성요소 간 공유 지식 정의
   응집력(cohesion) = "좋은 결합"
   모듈 간 관계 없이 모듈성 평가 불가

0. 사전 필수 용어

  • 모듈성 (Modularity) — 시스템이 독립적인 단위(모듈)로 구성되어 미래 변화에 유연하게 적응할 수 있는 설계 속성. 단순 분해가 아니라 합리적인 노력으로 진화 가능해야 함.
  • 모듈 (Module) — 잘 정의된 기능을 포괄하고, 그 기능을 다른 모듈에 제공하며, 독립적으로 컴파일 가능한 프로그램 문장의 모음(Myers 1979). 서비스·패키지·클래스·메서드 모두 해당.
  • 구성요소 (Component) — 시스템을 이루는 부분. 모듈은 구성요소의 일종이지만, 모든 구성요소가 모듈인 것은 아님.
  • 기능 (Function) — 모듈이 공개 인터페이스를 통해 외부에 드러내는 목표/기능.
  • 로직 (Logic) — 기능을 구현하는 내부 세부사항. 외부에 숨겨져야 함.
  • 맥락 (Context) — 모듈이 동작할 환경에 대한 명시적·암묵적 가정. 기능보다 덜 드러나지만 통합에 필수적.
  • 깊은 모듈 (Deep Module) — 좁은(간결한) 인터페이스 뒤에 복잡한 구현을 캡슐화한 모듈. 소비자가 내부를 몰라도 됨.
  • 얕은 모듈 (Shallow Module) — 인터페이스와 구현이 거의 동일한 모듈. 캡슐화가 사실상 없음.
  • 정보 은닉 (Information Hiding) — 파나스(1971) 제안. 설계 결정을 단일 모듈 안에 숨겨, 변경 시 그 모듈만 수정하면 되도록 하는 원칙.
  • 추상화 (Abstraction) — 여러 개체를 동등하게 표현하기 위해 공유하는 속성에만 집중하고 불필요한 세부사항은 제거한 개념.
  • 누출 추상화 (Leaking Abstraction) — 추상화가 숨겨야 할 기반 구현 세부사항이 외부로 새어 나오는 상태. 소비자가 내부를 알아야만 올바르게 쓸 수 있음.
  • 응집력 (Cohesion) — 모듈 내부 요소들이 서로 얼마나 긴밀하게 관련되어 있는지. "좋은 결합"이라고도 불림.
  • 큰 진흙덩이 (Big Ball of Mud) — 체계 없이 지식이 무분별하게 공유되는 모듈성의 반대 안티패턴(Foote and Yoder 1997).

1. 모듈성의 의미 — 왜 필요한가

모듈성은 소프트웨어 설계에서만 쓰이는 개념이 아니다. "모듈"이라는 용어는 소프트웨어보다 약 500년 앞서 존재했다. 본질적으로 모듈성이란 시스템이 독립적인 단위(모듈)로 구성된 상태를 말한다.

시스템에는 목표(현재 기능)가 있다. 하지만 시간이 지나면 사용자 요구가 바뀐다. 여기서 모듈성이 작용한다. 모듈식 설계는 현재 알려진 요구사항뿐 아니라 미래에 필요할 수도 있는 요구사항을 수용할 수 있도록 시스템을 확장한다.

"살아남는 종은 가장 강한 종이 아니라 가장 적응력이 강한 종이다." (다윈에게 귀속되나 출처 불분명) — 시스템에도 그대로 적용된다. 아무리 정교하게 설계된 시스템도 변화에 유연하게 대응하지 못하면 쓸모없어진다.

모듈성의 두 가지 주요 기여:

  1. 적응성 — 합리적인 노력으로 시스템을 진화시킬 수 있다.
  2. 인지 단순화 — 모놀리식 블랙박스 대신 이해 가능한 부품 집합으로 시스템을 표현한다.

시스템의 동작 방식을 깊이 이해하는 것은 수정·개선에 직결된다. 모듈식 설계의 단순함과 투명함 덕분에 더 효과적으로 자신 있게 고치고, 조정하고, 혁신할 수 있다.


2. 모듈과 구성요소의 차이

모든 시스템은 구성요소로 구성된다. 모듈은 구성요소의 일종이다. 그러나 모든 구성요소가 모듈인 것은 아니다. 유연한 시스템을 설계하려면 구성요소(모듈)를 결합·재구성·교체하여 시스템을 변경할 수 있어야 한다.

Myers(1979)에 따르면 소프트웨어 모듈은 다음 세 기준을 모두 충족한다.

  1. 독립적인 기능을 구현한다.
  2. 다른 모듈에서 호출할 수 있다.
  3. 독립적으로 컴파일될 가능성이 있다.

경계 유형(논리적 vs 물리적)보다 잘 정의된 기능이 더 중요하다. 서비스·패키지·클래스·함수 모두 모듈이 될 수 있다. 모듈 설계는 계층적이다("아래로 계속 거북이가 있는 식").

서비스
  └─ 패키지
       └─ 하위 패키지
            └─ 클래스
                 └─ 메서드
                      └─ 내부 문장들

각 층위마다 기능·로직·맥락의 세 속성이 동일하게 적용된다.


3. 모듈성의 일반 사례 — 레고와 카메라 렌즈

3.1 레고 블록

레고 블록은 모듈성의 교과서적 예다.

속성 레고 블록에서의 의미
기능 다른 블록과 연결한다 — 요철과 구멍이 통합 인터페이스
로직 무게를 지탱하고 안정적으로 부착되는 소재와 내부 구조
맥락 어린이용 장난감. 실제 집 건축에는 적합하지 않음

핵심: 각 블록은 독립적이지만 자유롭게 조합·분해된다. 새 구조를 만들기 위해 블록을 다시 설계할 필요가 없다. 이것이 모듈성의 힘이다.

3.2 교환식 카메라 렌즈

사진작가는 렌즈를 교체함으로써 여러 대의 카메라 없이 다양한 촬영 조건에 적응할 수 있다.

속성 카메라 렌즈에서의 의미
기능 특정 초점 거리·조리개로 사진 촬영 — 지원 카메라 종류와 광학 기능 범위가 인터페이스
로직 렌즈를 카메라에 연결해 광학 기능을 탑재하는 내부 작동 방식
맥락 지원되는 카메라 모델 범위, 자동 초점 지원 여부 등

3.3 추가 일상 비유

  • USB 포트 — 어떤 기기든 표준 인터페이스(기능)로 연결. 기기 내부 회로(로직)를 몰라도 된다. 단, "USB 2.0이냐 3.0이냐"라는 맥락은 알아야 한다.
  • 전기 콘센트 — 전압·주파수라는 표준 인터페이스 뒤로 발전 방식을 숨긴다. 220V 콘센트에 110V 기기를 꽂으면 맥락 불일치로 폭발한다.
  • PC 부품 — CPU·RAM·GPU를 각자 교체 가능. 마더보드(인터페이스)가 공통 기능을 정의하고, 각 부품 내부(로직)는 숨겨진다.

4. 소프트웨어 모듈의 기능·로직·맥락

4.1 기능 — 공개 인터페이스

기능은 소비자에게 명시적으로 노출되는 부분이다.

# 서비스 수준 예시 — REST API 가 기능을 노출
# GET /customers/{id}
# POST /customers
# GET /customers?name=홍길동

# 클래스 수준 예시
class CustomerRepository:
    def load(self, customer_id: CustomerId) -> Customer: ...
    def save(self, customer: Customer) -> None: ...
    def find_by_name(self, name: Name) -> list[Customer]: ...
    def find_by_phone(self, phone: PhoneNumber) -> list[Customer]: ...

이 인터페이스만 보면 어떤 데이터베이스를 쓰는지 전혀 알 수 없다. 그게 정상이다.

4.2 로직 — 숨겨진 구현

로직은 소비자에게 절대 노출돼서는 안 된다.

# 올바른 예: 로직이 클래스 내부에 캡슐화됨
class PostgreSQLCustomerRepository:
    def __init__(self, conn):
        self._conn = conn

    def save(self, customer: Customer) -> None:
        # 이 SQL 구문은 소비자가 알 필요 없음
        self._conn.execute(
            "INSERT INTO customers (id, name, phone) VALUES (%s, %s, %s)",
            (customer.id, customer.name, customer.phone)
        )

    def find_by_name(self, name: Name) -> list[Customer]:
        # 인덱스 전략, 쿼리 최적화도 내부 사항
        rows = self._conn.execute(
            "SELECT * FROM customers WHERE name = %s", (str(name),)
        )
        return [Customer.from_row(r) for r in rows]

DB 스키마를 바꿔도, 인덱스를 추가해도, 쿼리를 최적화해도 — 소비자 코드는 바꿀 필요가 없다.

4.3 맥락 — 환경 가정

맥락은 기능과 달리 공개 인터페이스에 잘 드러나지 않는다. 하지만 통합에 영향을 미친다.

# 맥락 예시 — 다음 가정들은 문서화해야 한다
# 1. Python 3.10+ 런타임 필요
# 2. 호출 전 인증이 완료되어 있다고 가정 (권한 부여 직접 수행 X)
# 3. 초당 최대 1000 쿼리를 처리 가능 (컴퓨팅 리소스 가정)

class CustomerRepository:
    """
    맥락:
    - 호출 스레드에서 DB 연결이 미리 열려 있어야 함
    - 저장 전에 customer.validate() 통과를 전제함
    """
    ...

맥락이 명시되지 않으면 암묵적 결합이 된다 — 1장에서 배운 가장 위험한 형태의 지식 공유.


5. 깊은 모듈 vs 얕은 모듈

오스터하우트(John Ousterhout)의 『A Philosophy of Software Design』(2018)에서 제시한 시각적 휴리스틱이다.

     좁은 인터페이스(기능)
     ─────────────────
    │                 │
    │                 │
    │    로직(구현)    │   ← 깊은 모듈: 인터페이스가 좁고
    │                 │     로직이 넓다 = 많이 숨긴다
    │                 │
    └─────────────────┘

     넓은 인터페이스(기능)
     ─────────────────────────────────────
    │  로직(구현)                         │  ← 얕은 모듈: 인터페이스와 로직이
    └─────────────────────────────────────┘    거의 같다 = 거의 숨기지 않는다

잘못된 예 — 얕은 모듈

# 얕은 모듈의 극단: addTwoNumbers(a, b) — 인터페이스 = 구현
def add_two_numbers(a: int, b: int) -> int:
    return a + b
# 소비자가 그냥 a + b 를 직접 쓰는 것과 차이 없음.
# 아무 복잡성도 캡슐화하지 않는다.

# 또 다른 얕은 모듈 예시 — getter/setter 만 있는 클래스
class CustomerDTO:
    def get_name(self) -> str:
        return self._name
    def set_name(self, name: str):
        self._name = name
    # 로직 없음, 검증 없음 = 클래스가 아니라 그냥 구조체

문제: 소비자가 이 "모듈"을 쓰는 것과 구현을 직접 보는 것 사이에 차이가 없다. 캡슐화의 이점이 0이다.

올바른 예 — 깊은 모듈

# 깊은 모듈 예시 — Unix 파일 시스템 (오스터하우트 예시)
# 소비자 입장에서 보이는 인터페이스 (아주 좁다)
# open(path, flags)
# read(fd, buffer, count)
# write(fd, buffer, count)
# close(fd)

# 하지만 내부 로직은 엄청나게 복잡하다:
# 파일 시스템 타입 판별, 권한 검사, 버퍼 캐시 관리,
# 디스크 블록 할당, 저널링, 심링크 처리 ...
# 소비자는 이 중 하나도 알 필요 없다.

# 소프트웨어 예시 — 깊은 저장소
class CustomerRepository:
    def save(self, customer: Customer) -> None:
        # 내부에서: 검증, 직렬화, 트랜잭션, 재시도, 인덱스 갱신 등
        self._validate(customer)
        with self._conn.transaction():
            self._conn.execute("UPSERT INTO customers ...", (...))
            self._update_search_index(customer)

    def find_by_name(self, name: Name) -> list[Customer]:
        # 내부에서: 캐시 확인, 쿼리 최적화, 역직렬화
        cached = self._cache.get(f"name:{name}")
        if cached:
            return cached
        rows = self._conn.execute("SELECT ...", (str(name),))
        result = [Customer.from_row(r) for r in rows]
        self._cache.set(f"name:{name}", result)
        return result

소비자가 아는 것: save(customer), find_by_name(name) 뿐. 트랜잭션·캐시·인덱스는 모른다.

깊은 모듈의 한계

깊은 모듈이라도 같은 비즈니스 규칙을 두 모듈이 중복 구현하면 하나 변경 시 양쪽 다 수정해야 한다. 깊음(depth)은 필요조건이지 충분조건이 아니다 — 중복 없는 단일 책임이 함께 필요하다.


6. 정보 은닉과 추상화 — 파나스의 원칙

6.1 파나스의 정보 은닉 (1971)

데이비드 파나스(David L. Parnas)는 1971년 논문에서 모듈 = 책임 할당이라고 정의하고, 효과적인 모듈을 결정을 숨기는 모듈이라 불렀다.

"결정을 다시 살펴봐야 하는 경우, 변경이 그것을 '숨기는' 모듈에만 영향을 미쳐야 한다." — Parnas(1971)

파나스 자신이 2023년 필자에게 보낸 편지에서 밝힌 실패 사례:

"1960년대 후반, 관리자들은 작업을 부분으로 나누면 '잘 맞아떨어질' 것이라 기대했다. 하지만 (a) 시스템이 잘 작동하지 않았고 (b) 변경은 단일 모듈에만 제한되지 않고 여러 모듈에 영향을 미쳤다. 이유: 모듈로 나눌 때 '잘못했기' 때문이다. 인터페이스가 너무 복잡했다." — Parnas(2023)

6.2 추상화의 본질

추상화는 여러 개체를 동등하게 잘 표현하기 위해 공유하는 속성에만 집중하고 나머지는 제거한다.

예시: '자동차'라는 단어는 추상화다. 색상·제조사·모델을 몰라도 된다. 대신 "4바퀴, 엔진, 핸들, 운송 제공"이라는 공통 속성에 집중한다.

"추상화의 목적은 모호한 것을 만드는 것이 아니라, 절대적으로 정확할 수 있는 새로운 의미 수준을 만드는 것이다." — 데이크스트라(1972)

추상화 수준 선택의 균형:

너무 좁은 추상화 (과소 일반화)
  예: "테슬라 모델 3 빨간색" → 특정 인스턴스만 표현, 재사용 불가

적절한 추상화
  예: "자동차" → 승용차·SUV·택시 모두 표현, 버스·오토바이는 X

너무 넓은 추상화 (과잉 일반화)
  예: "탈것" → 자동차·버스·오토바이·배·비행기까지. 오토바이와 자동차를
      동일하게 모델링해야 하는 상황이 실제로 있지 않으면 불필요

6.3 저장소 추상화 예시 (잘못된 예 vs 올바른 예)

잘못된 예 A — 구체 제품까지 노출

# 잘못된 예: CustomerService가 "MySQL"이라는 구체 DB를 직접 안다
class CustomerService:
    def __init__(self, repo: MySQLRepository):  # 구체 클래스에 의존
        self.repo = repo

    def register(self, customer):
        self.repo.execute_sql(
            "INSERT INTO customers (name, email) VALUES (%s, %s)",
            (customer.name, customer.email)
        )
# 문제: MySQL을 PostgreSQL로 바꾸면 CustomerService도 바꿔야 한다.
# DB 선택이라는 결정이 캡슐화되지 않고 밖으로 새어 나왔다.

잘못된 예 B — SQL 문법이 여전히 노출

# 중간 단계: 추상 인터페이스, 하지만 "SQL"이라는 가정이 남음
class IRepository:
    def begin_transaction(self): ...
    def execute_sql(self, query: str, params): ...  # SQL 가정 노출
    def commit(self): ...
    def rollback(self): ...

class CustomerService:
    def register(self, customer):
        self.repo.execute_sql(
            "INSERT INTO customers ...", (customer.name,)
        )
# 진전: 구체 제품은 모르지만, 관계형 DB 집단을 가정한다.
# MongoDB·Redis로 바꾸면 여전히 깨진다.

올바른 예 C — 의도만 노출

# 올바른 예: 오직 "무엇을 하는가"만 드러낸다
class ICustomerRepository:
    def save(self, customer: Customer) -> None: ...
    def find_by_name(self, name: Name) -> list[Customer]: ...
    def find_by_phone(self, phone: PhoneNumber) -> list[Customer]: ...

class CustomerService:
    def __init__(self, repo: ICustomerRepository):
        self.repo = repo

    def register(self, customer: Customer) -> None:
        self.repo.save(customer)
        # SQL도, 트랜잭션도, 인덱스도 모른다 — 의도만 안다

    def find_customer(self, name: str) -> list[Customer]:
        return self.repo.find_by_name(Name(name))

저장소를 MySQL→PostgreSQL→MongoDB→메모리 모의 객체(단위 테스트용)로 바꿔도 CustomerService는 단 한 줄도 바꾸지 않는다.


7. 모듈성 vs 복잡성의 대립

7.1 지식의 위치가 결정한다

모듈성과 복잡성은 모두 지식이 시스템에 어떻게 분산되느냐에서 비롯된다.

  • 모듈성 → 지식을 모듈 경계 안에 최적으로 분산. 외부엔 최소한만 노출.
  • 복잡성 → 지식이 구성요소 간에 무분별하게 공유됨. 어디서든 무엇이든 알 수 있는 상태.

모듈식 설계가 복잡성을 제어하는 두 가지 방식:

  1. 우발적 복잡성 제거 — 잘못된 설계에서 오는 복잡성을 없앰.
  2. 필수적 복잡성 관리 — 제거 불가능한 비즈니스 복잡성을 적절한 모듈에 캡슐화해 전역으로 퍼지지 않게 막음.

7.2 큰 진흙덩이 안티패턴

모듈성의 정반대가 큰 진흙덩이(Big Ball of Mud)다.

"체계 없이 무작위로 구조화된, 뒤죽박죽이고 엉성하며 임시방편으로 덕지덕지 이어 붙인 코드 정글. 정보는 서로 멀리 떨어진 요소들 사이에서 무분별하게 공유되며, 중요한 정보의 거의 모든 부분이 전역화되거나 중복된다." — Foote and Yoder(1997)

# 큰 진흙덩이의 전형적인 징후
# 잘못된 예: HTTP 핸들러가 DB 스키마와 비즈니스 규칙을 동시에 알고 있음
@app.post("/orders")
def create_order():
    body = request.json
    # DB 스키마를 직접 안다
    db.execute("INSERT INTO orders (user_id, items_json, created_at) ...", (...))
    # 이메일 서버를 직접 안다
    smtp.sendmail("[email protected]", body["email"], "주문 완료")
    # 결제 API를 직접 안다
    payment_api.charge(body["card_number"], body["amount"])
    return {"ok": True}

# 문제: 이 함수 하나가 DB 스키마·이메일 서버·결제 API를 모두 안다.
# DB 컬럼명 하나 바꾸면 이 함수가 깨진다.
# 올바른 예: 각 책임이 모듈 안에 캡슐화됨
@app.post("/orders")
def create_order():
    command = CreateOrderCommand.from_request(request)
    order = order_service.create(command)  # 비즈니스 로직 캡슐화
    notification_service.notify_created(order)  # 이메일 방식 캡슐화
    return OrderResponse.from_domain(order).to_dict()
    # HTTP 핸들러는 "무엇을 하는가"만 알고 "어떻게 하는가"는 모른다

7.3 모듈성의 과유불급

모듈식 설계는 합리적인 변화에 초점을 맞춰야 한다. 모든 가능한 변화를 허용하려 하면 인터페이스가 비대해지고 구현이 지나치게 복잡해진다.

재사용성이 높을수록 유용성이 떨어진다. 이것이 유연성의 비용이며, 결과적으로 모듈성의 비용이다.

두 가지 극단 모두 피해야 한다: - 시스템을 변경할 수 없을 정도로 경직되게 만드는 것 - 시스템을 사용할 수 없을 정도로 유연하게 설계하는 것


8. 누출 추상화

누출 추상화는 추상화가 숨겨야 할 내부 세부사항이 외부로 새어 나오는 상태다. Joel Spolsky의 "누출 추상화 법칙(2002)"에서 정의됐다.

소비자가 "이 모듈을 올바르게 쓰려면 내부를 알아야 한다"는 상황이 누출 추상화다.

잘못된 예 — 트랜잭션 의미론이 누출된 추상화

# 잘못된 예: 트랜잭션 경계가 인터페이스에 드러나지 않아 누출 발생
class DataLayer:
    def save_customer(self, customer): ...
    def save_order(self, order): ...
    # 인터페이스만 보면 두 save가 원자적인지 알 수 없다

# 소비자가 어쩔 수 없이 내부를 알아야 함
service = DataLayer()
service.save_customer(customer)  # 여기서 commit이 되나?
service.save_order(order)        # 이것도 따로 commit인가?
# → 소비자가 DB 트랜잭션 방식을 알아야만 올바르게 쓸 수 있다
# 올바른 예: 트랜잭션 의미론을 인터페이스에 명시
class OrderFulfillmentService:
    def fulfill(self, customer: Customer, order: Order) -> None:
        """
        고객 등록과 주문 생성을 원자적으로 처리한다.
        둘 중 하나라도 실패하면 전체 롤백.
        """
        with self._unit_of_work:
            self._customer_repo.save(customer)
            self._order_repo.save(order)
# 소비자는 원자성을 보장받는다는 것만 알면 된다 — 내부 DB 구현 무관

잘못된 예 — 성능 특성 누출

# 잘못된 예: 페이지네이션 없는 find_all — 수백만 건 로드 가능성
class ProductRepository:
    def find_all(self) -> list[Product]:
        return self._db.execute("SELECT * FROM products")
        # 소비자가 "이게 얼마나 많은 데이터를 로드할지" 알아야 쓸 수 있다

# 올바른 예: 성능 계약을 인터페이스에 내장
class ProductRepository:
    def find_page(self, page: int, size: int = 50) -> Page[Product]:
        """
        최대 size개의 제품을 반환. 전체 개수는 Page.total로 접근.
        """
        ...

9. 모듈성에서 결합의 역할

9.1 모듈은 고립으로 평가할 수 없다

개별 모듈을 아무리 잘 설계해도, 모듈 간 상호작용이 나쁘면 시스템 전체는 비모듈식이다.

앨런 케이(Alan Kay): "객체 지향 프로그래밍의 큰 아이디어는 클래스가 아니라 메시징이다." — 객체(모듈) 자체가 아니라 객체 간의 관계와 상호작용이 중요하다는 뜻.

전통적으로 설계할 때 "상자(구성요소)"에만 집중했다. 하지만 상자를 연결하는 "화살표와 선(상호작용)"은 어떤가?

9.2 결합 = 공유 지식 정의자

[ 모듈성의 두 축 ]

          결합                응집력
    (구성요소 간 관계)    (모듈 내부 요소 간 관계)
          │                      │
          ▼                      ▼
  구성요소 간에 어떤       모듈의 책임이 서로
  지식이 공유되는가        얼마나 관련 있는가
          │
          ▼
  ┌─────────────────────────────────────┐
  │ 복잡성 증가 결합 / 모듈성 기여 결합 │
  └─────────────────────────────────────┘

결합이 공유하는 지식의 종류와 양에 따라: - 과잉 공유 → 복잡성 증가 (큰 진흙덩이) - 최소 공유 → 모듈성 기여

응집력은 흔히 "좋은 결합"이라 부른다. 모듈 내부 요소들이 서로 긴밀하게 관련될수록(높은 응집력) 단일하고 잘 정의된 목적을 달성한다.

9.3 결합 방식이 모듈성을 결정하는 코드 예시

# 잘못된 예: 두 모듈이 DB 스키마라는 지식을 공유 — 복잡성 결합
class OrderService:
    def place(self, order):
        db.execute("INSERT INTO orders (user_id, items_json) ...", (...))
        # orders 테이블 스키마를 직접 안다

class ReportService:
    def monthly_summary(self):
        rows = db.execute("SELECT items_json FROM orders ...")
        # 여기도 items_json 컬럼명을 직접 안다

# 문제: items_json → items 로 컬럼명 바꾸면 두 서비스 모두 깨진다.
# 지식이 두 모듈에 분산돼 있다.

# 올바른 예: 지식을 OrderRepository 한 곳에 캡슐화 — 모듈성 기여 결합
class OrderRepository:
    def save(self, order: Order) -> None:
        db.execute("INSERT INTO orders (user_id, items_json) ...", (...))

    def monthly_totals(self, year: int, month: int) -> list[MonthlyTotal]:
        rows = db.execute("SELECT items_json FROM orders WHERE ...", (...))
        return [MonthlyTotal.from_row(r) for r in rows]

class OrderService:
    def place(self, order):
        self._repo.save(order)  # items_json을 모른다

class ReportService:
    def monthly_summary(self):
        return self._repo.monthly_totals(2026, 5)  # items_json을 모른다

# 컬럼명 변경 → OrderRepository 한 곳만 수정

핵심 개념 정리

개념 한 줄 설명
모듈성(modularity) 시스템이 독립적인 단위로 구성되어 미래 변화에 적응할 수 있는 설계 속성
모듈(module) 잘 정의된 기능을 캡슐화하고 외부에 노출하며 독립 컴파일 가능한 단위
구성요소(component) 시스템을 이루는 부분. 모듈의 상위 개념
기능(function) 공개 인터페이스를 통해 노출된 모듈의 목표. 명시적
로직(logic) 기능 구현 방법. 외부에 숨겨져야 함
맥락(context) 모듈의 동작 환경 가정. 암묵적일 수 있어 주의 필요
깊은 모듈 좁은 인터페이스 + 복잡한 내부 = 많이 숨기는 모듈. 바람직함
얕은 모듈 인터페이스 ≈ 구현. 캡슐화 없음. 회피 대상
정보 은닉 설계 결정을 모듈 내부에 가둬 변경이 외부에 전파되지 않게 하는 원칙(Parnas 1971)
추상화 공유하는 세부사항에만 집중해 여러 개체를 동등하게 표현
누출 추상화 숨겨야 할 구현 세부사항이 인터페이스 밖으로 새어 나오는 상태
큰 진흙덩이 지식이 무분별하게 공유되는 모듈성 반대 안티패턴
응집력(cohesion) 모듈 내부 요소들의 관련도. "좋은 결합"으로도 불림
결합과 모듈성 결합이 공유하는 지식의 종류/양이 모듈성 vs 복잡성을 결정
과유불급 과도한 유연성은 복잡성을 낳는다 — 합리적인 자유도만 노출

실무 체크리스트

  • [ ] 새 모듈을 만들 때 기능·로직·맥락 세 가지를 각각 명시적으로 정의했는가?
  • [ ] 공개 인터페이스가 구체 구현(DB 이름, SQL 문법, 파일 경로)을 노출하지 않고 의도만 드러내는가?
  • [ ] 인터페이스와 구현이 거의 동일한 "얕은 모듈"이 있다면, 합칠 수 있거나 제거할 수 있는지 검토했는가?
  • [ ] 모듈의 맥락(환경 가정)이 문서화되어 있는가? 암묵적 가정이 인터페이스나 문서로 끌어올려졌는가?
  • [ ] 두 모듈이 같은 지식(컬럼명, 스키마, 프로토콜)을 각자 알고 있다면, 그 지식을 단일 모듈에 캡슐화했는가?
  • [ ] 소비자가 이 모듈을 올바르게 사용하려면 내부를 알아야 하는 누출 추상화가 없는가?
  • [ ] 인터페이스를 변경하지 않고 내부(로직)만 바꿀 수 있는가? 소비자 코드가 깨지지 않는지 테스트로 확인했는가?
  • [ ] 모듈 유연성이 과하지 않은가? 합리적인 변화에만 열려 있고, 필요 없는 자유도는 제거했는가?
  • [ ] 깊은 모듈을 설계했더라도, 같은 비즈니스 규칙이 두 모듈에 중복 구현되지 않았는가?
  • [ ] 미래 변화를 예측하기 위해 비즈니스 도메인 추세·경쟁 제품·과거 변경 이력을 분석했는가?

연습문제 (문제만 — 정답은 부록 D)

  1. 개념. 모듈과 구성요소의 차이를 Myers(1979)의 세 기준으로 설명하라. "모든 모듈은 구성요소이지만, 모든 구성요소가 모듈은 아니다"는 말이 어떤 의미인지 예시를 들어 답하라.

  2. 분석. 다음 인터페이스의 세 가지 속성(기능·로직·맥락)을 각각 식별하라. python class EmailNotifier: def send_welcome(self, user_email: str, user_name: str) -> None: ... def send_order_confirmed(self, user_email: str, order_id: str) -> None: ... # 단: 내부적으로 SendGrid API를 사용하고, 초당 100통 이상 보내면 레이트 리밋 발생

  3. 설계. 다음 얕은 모듈을 깊은 모듈로 리팩토링하라. 현재 인터페이스가 어떤 면에서 얕은지 설명하고, 개선된 인터페이스를 코드로 제시하라. python class DatabaseLogger: def log(self, table: str, columns: list, values: list): sql = f"INSERT INTO {table} ({','.join(columns)}) VALUES ({','.join(['?']*len(values))})" db.execute(sql, values)

  4. 식별. 다음 코드에서 누출 추상화를 찾고, 왜 누출인지 설명하고, 수정하라. ```python class CacheService: def get(self, key: str) -> str | None: # Redis TTL이 만료되면 None 반환, # 네트워크 오류 시 RedisConnectionError 발생 return self._redis.get(key)

# 소비자 코드 val = cache.get("user:123") if val is None: # Redis가 만료한 건지, 처음부터 없는 건지 알 수 없음 ... ```

  1. 적용. 쇼핑몰 시스템에서 다음 세 모듈 사이의 결합을 분석하라: ProductCatalogService, InventoryService, PricingService. 각 모듈이 다른 모듈에 대해 어떤 지식을 공유해야 하는지 설계하고, 복잡성을 유발하는 결합과 모듈성에 기여하는 결합을 각각 예시 코드로 제시하라.

최신 동향 (2026-05 기준)

최신 동향 (검증 2026-05-26)

  • 도메인 주도 설계(DDD)와 모듈성의 통합 가속화. 파나스의 정보 은닉 원칙이 바운디드 컨텍스트(Bounded Context) 개념과 자연스럽게 맞물려, ContextMapper 같은 도구가 DDD 모듈 경계를 시각화·자동 리팩토링하는 데 쓰인다.
  • 패키지/모듈 경계 정적 분석 도구 성숙. ArchUnit(Java), Dependency Cruiser(Node.js), import-linter(Python) 등이 "이 패키지는 저 패키지를 알면 안 된다"는 규칙을 CI 단계에서 강제해, 누출 추상화와 큰 진흙덩이를 자동으로 감지한다.
  • 모듈성 평가 지표의 도구화: 깊은 모듈 개념을 정량화하는 시도가 늘고 있다. 인터페이스 표면적 대비 구현 복잡도 비율을 측정하는 실험적 메트릭이 연구되고 있으며, SonarQube 같은 정적 분석 도구에 관련 규칙이 추가되고 있다.

부록 A. 용어 사전

한글 용어 원문 영문명 의미
모듈성 Modularity 시스템이 독립적인 모듈로 구성되어 미래 변화에 유연하게 적응할 수 있는 설계 속성
모듈 Module 잘 정의된 기능을 캡슐화하고 다른 모듈에 노출하며 독립 컴파일 가능한 단위
구성요소 Component 시스템을 이루는 부분. 모듈의 상위 개념
기능 Function 모듈이 공개 인터페이스를 통해 드러내는 목표
로직 Logic 기능을 구현하는 내부 세부사항. 외부에 숨겨짐
맥락 Context 모듈의 동작 환경에 대한 명시적·암묵적 가정
깊은 모듈 Deep module 좁은 인터페이스 뒤에 복잡한 구현을 캡슐화한 모듈
얕은 모듈 Shallow module 인터페이스와 구현이 거의 동일해 캡슐화가 없는 모듈
정보 은닉 Information hiding 설계 결정을 모듈 내부에 가둬 변경 전파를 최소화하는 원칙(Parnas 1971)
추상화 Abstraction 공유하는 속성에만 집중해 여러 개체를 동등하게 표현하는 개념
누출 추상화 Leaking abstraction 숨겨야 할 구현 세부사항이 인터페이스 밖으로 새어 나오는 상태
큰 진흙덩이 Big Ball of Mud 지식이 무분별하게 공유되는 모듈성 반대 안티패턴(Foote and Yoder 1997)
응집력 Cohesion 모듈 내부 요소들의 관련도. 높을수록 바람직. "좋은 결합"으로도 불림
과유불급 Over-engineering flexibility 과도한 유연성이 복잡성을 낳는 함정

부록 B. 핵심 비교표

깊은 모듈 vs 얕은 모듈

구분 깊은 모듈 얕은 모듈
인터페이스 크기 좁고 간결 넓고 장황
로직(구현) 복잡도 높음 낮음
캡슐화 효과 없음에 가까움
변경 전파 인터페이스 유지 시 없음 구현 바꾸면 소비자까지 전파
예시 Unix open/read/write add_two_numbers(a, b): return a+b
가치 소비자가 내부를 몰라도 됨 그냥 구현 직접 쓰는 것과 동일

모듈성 vs 복잡성

구분 모듈성 복잡성
지식 분산 방식 모듈 경계 내 캡슐화, 외부엔 최소 노출 구성요소 간 무분별 공유
변경 시 영향 관련 모듈 하나만 수정 여러 구성요소에 연쇄 수정
인지 부하 낮음 (의미 수준 분리) 높음 (전체를 알아야 부분을 이해)
대표 패턴 정보 은닉, 깊은 모듈, 추상화 큰 진흙덩이, 전역 상태, 강한 결합

정보 은닉이 잘된 추상화 vs 누출 추상화

구분 정보 은닉 잘된 추상화 누출 추상화
소비자가 알아야 하는 것 인터페이스(기능)만 내부 구현도
구현 교체 시 소비자 코드 변경 불필요 변경 필요
오류 처리 추상화 수준에 맞는 예외 내부 기술 예외 노출 (예: RedisConnectionError)
예시 repo.save(customer) db.execute("INSERT INTO ...", ...) 직접 노출

모듈 세 가지 속성 비교

속성 반영 대상 정보 형태 공개 여부
기능 모듈의 목표 공개 인터페이스 명시적 공개
로직 구현 방법 내부 코드·설계 결정 모듈 내부에 숨겨짐
맥락 환경 가정 문서·전제 조건 공개되나 기능보다 덜 명시적

부록 C. 추천 참고 자료 & 링크

Tier 1 공식·표준 (생존 확인 2026-05-26)

자료 링크
책 공식 (제이펍) jpub.kr
원서 출판사 — Manning Balancing Coupling in Software Design
Parnas 1971 — 정보 은닉 원전 On the Criteria To Be Used in Decomposing Systems into Modules (ACM)
Ousterhout — 깊은 모듈 개념 원전 A Philosophy of Software Design (Stanford)
Spolsky — 누출 추상화 법칙(2002) The Law of Leaky Abstractions (Joel on Software)
Foote & Yoder — 큰 진흙덩이 원전 Big Ball of Mud (laputan.org)
ContextMapper — DDD 바운디드 컨텍스트 도구 contextmapper.org
ArchUnit — Java 모듈 경계 강제 archunit.org

책 다른 장 안내

자료 설명
1장 결합이란 무엇인가 — 어원·공유 수명주기·공유 지식
2장 커네빈 프레임워크로 복잡성 정의
3장 자유도·제약으로 본 복잡성과 상호작용
5장 이후 결합을 설계 도구로 사용하는 구체적 방법 (결합 강도·공생성·거리·변동성)

부록 D. 연습문제 풀이

1. 모듈과 구성요소의 차이

Myers(1979) 세 기준: (a) 독립적인 기능 구현, (b) 다른 모듈에서 호출 가능, (c) 독립 컴파일 가능성. 모든 구성요소는 시스템의 부분이지만, 모듈은 이 세 기준을 충족하는 구성요소다.

예시: 한 클래스 안의 private 메서드 _calculate_tax()는 구성요소지만 외부에서 호출 불가 → 모듈 아님. 반면 public 메서드 calculate_invoice()는 외부에서 호출 가능하고 독립 테스트 가능 → 모듈. 즉, 경계 유형(파일·패키지·서비스)보다 잘 정의된 기능과 독립성이 모듈을 구성요소와 구분한다.

2. EmailNotifier의 세 가지 속성

  • 기능: send_welcome(email, name) · send_order_confirmed(email, order_id) — 공개 인터페이스를 통해 드러난 두 이메일 발송 기능. 소비자가 알아야 하는 것.
  • 로직: SendGrid API 호출, HTTP 요청 형성, 재시도 로직 등 — 내부 구현. 소비자가 몰라도 되며 숨겨야 한다.
  • 맥락: "초당 100통 이상은 레이트 리밋 발생" — 이 환경 가정은 공개 인터페이스에 드러나지 않지만, 소비자가 대량 발송 루프를 짤 때 반드시 알아야 한다. 맥락이 명시되지 않으면 암묵적 결합이 된다. 해결: 문서화하거나, 인터페이스에 배치 발송 기능(예: send_batch(emails, max_per_sec=50))을 추가.

3. DatabaseLogger 리팩토링

현재 인터페이스가 얕은 이유: 소비자가 table, columns, values를 직접 조립해서 넘겨야 한다. 이는 SQL 구조를 소비자에게 노출한 것 — 로직이 캡슐화되지 않음. 컬럼 순서나 테이블 이름이 바뀌면 소비자 코드가 깨진다.

# 개선된 깊은 모듈: "무엇을 로깅할지"만 인터페이스에 드러냄
class AuditLogger:
    def log_user_action(self, user_id: str, action: str, detail: str) -> None:
        """사용자 행위를 감사 로그에 기록한다."""
        # 어떤 테이블, 어떤 컬럼인지는 내부에서만 안다
        db.execute(
            "INSERT INTO audit_log (user_id, action, detail, created_at) VALUES (?,?,?,?)",
            (user_id, action, detail, datetime.now())
        )

    def log_system_event(self, event_type: str, payload: dict) -> None:
        """시스템 이벤트를 기록한다. payload는 JSON으로 직렬화됨."""
        db.execute(
            "INSERT INTO system_events (event_type, payload_json, ts) VALUES (?,?,?)",
            (event_type, json.dumps(payload), datetime.now())
        )

소비자는 DB 스키마를 전혀 모른다. 테이블 이름이나 컬럼이 바뀌어도 소비자 코드는 변경 불필요.

4. CacheService 누출 추상화 수정

누출 이유: (a) 소비자가 None이 "만료됨"인지 "없음"인지 구분 불가 — 두 상황의 의미가 다름에도 동일하게 처리됨. (b) RedisConnectionError 같은 내부 기술 예외가 소비자에게 노출됨 — Redis를 Memcached로 바꾸면 소비자 코드의 예외 처리도 바꿔야 함.

# 올바른 예: 추상화 수준에 맞는 인터페이스 제공
from dataclasses import dataclass
from enum import Enum

class CacheMissReason(Enum):
    NOT_FOUND = "not_found"
    EXPIRED = "expired"

@dataclass
class CacheResult:
    hit: bool
    value: str | None
    miss_reason: CacheMissReason | None = None

class CacheService:
    def get(self, key: str) -> CacheResult:
        """
        캐시에서 값을 조회한다.
        - 히트: CacheResult(hit=True, value=...)
        - 만료: CacheResult(hit=False, miss_reason=EXPIRED)
        - 없음: CacheResult(hit=False, miss_reason=NOT_FOUND)
        - 네트워크 오류: CacheUnavailableError 발생 (Redis 세부사항 노출 X)
        """
        try:
            raw = self._redis.get(key)
            ...
        except redis.RedisConnectionError as e:
            raise CacheUnavailableError("캐시 서비스에 연결할 수 없습니다") from e

소비자는 Redis를 모른다. CacheUnavailableError·CacheMissReason은 추상화 수준의 언어다.

5. 쇼핑몰 세 모듈 간 결합 분석

복잡성을 유발하는 결합 (잘못된 예):

# ProductCatalogService가 PricingService 내부 가격 계산 공식을 안다
class ProductCatalogService:
    def get_display_price(self, product_id: str, user_tier: str):
        base_price = self._db.execute("SELECT base_price FROM products ...")
        # 가격 계산 공식을 직접 구현 — PricingService와 중복
        if user_tier == "GOLD":
            return base_price * 0.9
        return base_price

모듈성에 기여하는 결합 (올바른 예):

# ProductCatalogService는 가격이 필요하면 PricingService의 인터페이스를 통한다
class ProductCatalogService:
    def __init__(self, pricing: IPricingService, inventory: IInventoryService):
        self._pricing = pricing
        self._inventory = inventory

    def get_product_detail(self, product_id: str, user_id: str) -> ProductDetail:
        product = self._load_product(product_id)
        price = self._pricing.get_price(product_id, user_id)       # 가격 방식 모름
        stock = self._inventory.get_available_stock(product_id)    # 재고 관리 모름
        return ProductDetail(product, price, stock)

# 각 서비스는 자신의 책임(가격 정책, 재고 관리)만 알고,
# 다른 서비스의 구현 방식에는 의존하지 않는다.
# 할인 정책이 바뀌면 PricingService만 수정.
# 재고 시스템이 바뀌면 InventoryService만 수정.
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플